WaveFileCreator.fromScratch   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
1
2
/*
3
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
4
 *
5
 * Permission is hereby granted, free of charge, to any person obtaining
6
 * a copy of this software and associated documentation files (the
7
 * "Software"), to deal in the Software without restriction, including
8
 * without limitation the rights to use, copy, modify, merge, publish,
9
 * distribute, sublicense, and/or sell copies of the Software, and to
10
 * permit persons to whom the Software is furnished to do so, subject to
11
 * the following conditions:
12
 *
13
 * The above copyright notice and this permission notice shall be
14
 * included in all copies or substantial portions of the Software.
15
 *
16
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
 *
24
 */
25
26
/**
27
 * @fileoverview The WaveFileCreator class.
28
 * @see https://github.com/rochars/wavefile
29
 */
30
31
import { WaveFileParser } from 'wavefile-parser';
32
import { packArrayTo } from 'byte-data';
33
34
/** @module wavefile-creator */
35
36
/**
37
 * A class to read, write and create wav files.
38
 * @extends WaveFileParser
39
 */
40
export class WaveFileCreator extends WaveFileParser {
41
42
  /**
43
   * @param {?Uint8Array=} wavBuffer A wave file buffer.
44
   * @param {boolean=} samples True if the samples should be loaded.
45
   * @throws {Error} If container is not RIFF, RIFX or RF64.
46
   * @throws {Error} If format is not WAVE.
47
   * @throws {Error} If no 'fmt ' chunk is found.
48
   * @throws {Error} If no 'data' chunk is found.
49
   */
50
  constructor(wavBuffer=null, samples=true) {
51
    // Dont load the file yet
52
    super();
53
    /**
54
     * The bit depth code according to the samples.
55
     * @type {string}
56
     */
57
    this.bitDepth = '0';
58
    /**
59
     * @type {{
60
        be: boolean,
61
        bits: number,
62
        fp: boolean,
63
        signed: boolean
64
      }}
65
     * @protected
66
     */
67
    this.dataType = {
68
      be: false,
69
      bits: 0,
70
      fp: false,
71
      signed: false
72
    };
73
    /**
74
     * Audio formats.
75
     * Formats not listed here should be set to 65534,
76
     * the code for WAVE_FORMAT_EXTENSIBLE
77
     * @enum {number}
78
     * @protected
79
     */
80
    this.WAV_AUDIO_FORMATS = {
81
      '4': 17,
82
      '8': 1,
83
      '8a': 6,
84
      '8m': 7,
85
      '16': 1,
86
      '24': 1,
87
      '32': 1,
88
      '32f': 3,
89
      '64': 3
90
    };
91
    // Now load the file
92
    if (wavBuffer) {
93
      this.fromBuffer(wavBuffer, samples);
94
    }
95
  }
96
97
  /**
98
   * Set up the WaveFileCreator object from a byte buffer.
99
   * @param {!Uint8Array} wavBuffer The buffer.
100
   * @param {boolean=} samples True if the samples should be loaded.
101
   * @throws {Error} If container is not RIFF, RIFX or RF64.
102
   * @throws {Error} If format is not WAVE.
103
   * @throws {Error} If no 'fmt ' chunk is found.
104
   * @throws {Error} If no 'data' chunk is found.
105
   */
106
  fromBuffer(wavBuffer, samples=true) {
107
    super.fromBuffer(wavBuffer, samples);
108
    this.bitDepthFromFmt_();
109
    this.updateDataType_();
110
  }
111
112
  /**
113
   * Return a byte buffer representig the WaveFileCreator object as a .wav file.
114
   * The return value of this method can be written straight to disk.
115
   * @return {!Uint8Array} A wav file.
116
   * @throws {Error} If bit depth is invalid.
117
   * @throws {Error} If the number of channels is invalid.
118
   * @throws {Error} If the sample rate is invalid.
119
   */
120
  toBuffer() {
121
    this.validateWavHeader_();
122
    return super.toBuffer();
123
  }
124
  
125
  /**
126
   * Set up the WaveFileCreator object based on the arguments passed.
127
   * Existing chunks are reset.
128
   * @param {number} numChannels The number of channels
129
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
130
   * @param {number} sampleRate The sample rate.
131
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
132
   * @param {string} bitDepthCode The audio bit depth code.
133
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
134
   *    or any value between '8' and '32' (like '12').
135
   * @param {!Array<number>|!Array<!Array<number>>|!TypedArray} samples
136
   *    The samples. Must be in the correct range according to the bit depth.
137
   * @param {?Object} options Optional. Used to force the container
138
   *    as RIFX with {'container': 'RIFX'}
139
   * @throws {Error} If any argument does not meet the criteria.
140
   */
141
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
142
    // reset all chunks
143
    this.clearHeaders();
144
    this.newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options);
145
  }
146
147
  /**
148
   * Set up the WaveFileCreator object based on the arguments passed.
149
   * @param {number} numChannels The number of channels
150
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
151
   * @param {number} sampleRate The sample rate.
152
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
153
   * @param {string} bitDepthCode The audio bit depth code.
154
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
155
   *    or any value between '8' and '32' (like '12').
156
   * @param {!Array<number>|!Array<!Array<number>>|!TypedArray} samples
157
   *    The samples. Must be in the correct range according to the bit depth.
158
   * @param {?Object} options Optional. Used to force the container
159
   *    as RIFX with {'container': 'RIFX'}
160
   * @throws {Error} If any argument does not meet the criteria.
161
   * @private
162
   */
163
  newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options={}) {
164
    if (!options.container) {
165
      options.container = 'RIFF';
166
    }
167
    this.container = options.container;
168
    this.bitDepth = bitDepthCode;
169
    samples = interleave(samples);
170
    this.updateDataType_();
171
    /** @type {number} */
172
    let numBytes = this.dataType.bits / 8;
173
    this.data.samples = new Uint8Array(samples.length * numBytes);
174
    packArrayTo(samples, this.dataType, this.data.samples);
175
    this.makeWavHeader_(
176
      bitDepthCode, numChannels, sampleRate,
177
      numBytes, this.data.samples.length, options);
178
    this.data.chunkId = 'data';
179
    this.data.chunkSize = this.data.samples.length;
180
    this.validateWavHeader_();
181
  }
182
183
  /**
184
   * Define the header of a wav file.
185
   * @param {string} bitDepthCode The audio bit depth
186
   * @param {number} numChannels The number of channels
187
   * @param {number} sampleRate The sample rate.
188
   * @param {number} numBytes The number of bytes each sample use.
189
   * @param {number} samplesLength The length of the samples in bytes.
190
   * @param {!Object} options The extra options, like container defintion.
191
   * @private
192
   */
193
  makeWavHeader_(
194
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
195
    if (bitDepthCode == '4') {
196
      this.createADPCMHeader_(
197
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
198
199
    } else if (bitDepthCode == '8a' || bitDepthCode == '8m') {
200
      this.createALawMulawHeader_(
201
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
202
203
    } else if(Object.keys(this.WAV_AUDIO_FORMATS).indexOf(bitDepthCode) == -1 ||
204
        numChannels > 2) {
205
      this.createExtensibleHeader_(
206
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
207
208
    } else {
209
      this.createPCMHeader_(
210
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);      
211
    }
212
  }
213
214
  /**
215
   * Create the header of a linear PCM wave file.
216
   * @param {string} bitDepthCode The audio bit depth
217
   * @param {number} numChannels The number of channels
218
   * @param {number} sampleRate The sample rate.
219
   * @param {number} numBytes The number of bytes each sample use.
220
   * @param {number} samplesLength The length of the samples in bytes.
221
   * @param {!Object} options The extra options, like container defintion.
222
   * @private
223
   */
224
  createPCMHeader_(
225
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
226
    this.container = options.container;
227
    this.chunkSize = 36 + samplesLength;
228
    this.format = 'WAVE';
229
    this.bitDepth = bitDepthCode;
230
    this.fmt = {
231
      chunkId: 'fmt ',
232
      chunkSize: 16,
233
      audioFormat: this.WAV_AUDIO_FORMATS[bitDepthCode] || 65534,
234
      numChannels: numChannels,
235
      sampleRate: sampleRate,
236
      byteRate: (numChannels * numBytes) * sampleRate,
237
      blockAlign: numChannels * numBytes,
238
      bitsPerSample: parseInt(bitDepthCode, 10),
239
      cbSize: 0,
240
      validBitsPerSample: 0,
241
      dwChannelMask: 0,
242
      subformat: []
243
    };
244
  }
245
246
  /**
247
   * Create the header of a ADPCM wave file.
248
   * @param {string} bitDepthCode The audio bit depth
249
   * @param {number} numChannels The number of channels
250
   * @param {number} sampleRate The sample rate.
251
   * @param {number} numBytes The number of bytes each sample use.
252
   * @param {number} samplesLength The length of the samples in bytes.
253
   * @param {!Object} options The extra options, like container defintion.
254
   * @private
255
   */
256
  createADPCMHeader_(
257
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
258
    this.createPCMHeader_(
259
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
260
    this.chunkSize = 40 + samplesLength;
261
    this.fmt.chunkSize = 20;
262
    this.fmt.byteRate = 4055;
263
    this.fmt.blockAlign = 256;
264
    this.fmt.bitsPerSample = 4;
265
    this.fmt.cbSize = 2;
266
    this.fmt.validBitsPerSample = 505;
267
    this.fact = {
268
      chunkId: 'fact',
269
      chunkSize: 4,
270
      dwSampleLength: samplesLength * 2
271
    };
272
  }
273
274
  /**
275
   * Create the header of WAVE_FORMAT_EXTENSIBLE file.
276
   * @param {string} bitDepthCode The audio bit depth
277
   * @param {number} numChannels The number of channels
278
   * @param {number} sampleRate The sample rate.
279
   * @param {number} numBytes The number of bytes each sample use.
280
   * @param {number} samplesLength The length of the samples in bytes.
281
   * @param {!Object} options The extra options, like container defintion.
282
   * @private
283
   */
284
  createExtensibleHeader_(
285
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
286
    this.createPCMHeader_(
287
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
288
    this.chunkSize = 36 + 24 + samplesLength;
289
    this.fmt.chunkSize = 40;
290
    this.fmt.bitsPerSample = ((parseInt(bitDepthCode, 10) - 1) | 7) + 1;
291
    this.fmt.cbSize = 22;
292
    this.fmt.validBitsPerSample = parseInt(bitDepthCode, 10);
293
    this.fmt.dwChannelMask = dwChannelMask(numChannels);
294
    // subformat 128-bit GUID as 4 32-bit values
295
    // only supports uncompressed integer PCM samples
296
    this.fmt.subformat = [1, 1048576, 2852126848, 1905997824];
297
  }
298
299
  /**
300
   * Create the header of mu-Law and A-Law wave files.
301
   * @param {string} bitDepthCode The audio bit depth
302
   * @param {number} numChannels The number of channels
303
   * @param {number} sampleRate The sample rate.
304
   * @param {number} numBytes The number of bytes each sample use.
305
   * @param {number} samplesLength The length of the samples in bytes.
306
   * @param {!Object} options The extra options, like container defintion.
307
   * @private
308
   */
309
  createALawMulawHeader_(
310
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
311
    this.createPCMHeader_(
312
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
313
    this.chunkSize = 40 + samplesLength;
314
    this.fmt.chunkSize = 20;
315
    this.fmt.cbSize = 2;
316
    this.fmt.validBitsPerSample = 8;
317
    this.fact = {
318
      chunkId: 'fact',
319
      chunkSize: 4,
320
      dwSampleLength: samplesLength
321
    };
322
  }
323
324
  /**
325
   * Set the string code of the bit depth based on the 'fmt ' chunk.
326
   * @private
327
   */
328
  bitDepthFromFmt_() {
329
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
330
      this.bitDepth = '32f';
331
    } else if (this.fmt.audioFormat === 6) {
332
      this.bitDepth = '8a';
333
    } else if (this.fmt.audioFormat === 7) {
334
      this.bitDepth = '8m';
335
    } else {
336
      this.bitDepth = '' + this.fmt.bitsPerSample;
337
    }
338
  }
339
340
  /**
341
   * Validate the bit depth.
342
   * @return {boolean} True is the bit depth is valid.
343
   * @throws {Error} If bit depth is invalid.
344
   * @private
345
   */
346
  validateBitDepth_() {
347
    if (!this.WAV_AUDIO_FORMATS[this.bitDepth]) {
348
      if (parseInt(this.bitDepth, 10) > 8 &&
349
          parseInt(this.bitDepth, 10) < 54) {
350
        return true;
351
      }
352
      throw new Error('Invalid bit depth.');
353
    }
354
    return true;
355
  }
356
357
  /**
358
   * Update the type definition used to read and write the samples.
359
   * @private
360
   */
361
  updateDataType_() {
362
    this.dataType = {
363
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
364
      fp: this.bitDepth == '32f' || this.bitDepth == '64',
365
      signed: this.bitDepth != '8',
366
      be: this.container == 'RIFX'
367
    };
368
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
369
      this.dataType.bits = 8;
370
      this.dataType.signed = false;
371
    }
372
  }
373
374
  /**
375
   * Validate the header of the file.
376
   * @throws {Error} If bit depth is invalid.
377
   * @throws {Error} If the number of channels is invalid.
378
   * @throws {Error} If the sample rate is invalid.
379
   * @ignore
380
   * @private
381
   */
382
  validateWavHeader_() {
383
    this.validateBitDepth_();
384
    if (!validateNumChannels(this.fmt.numChannels, this.fmt.bitsPerSample)) {
385
      throw new Error('Invalid number of channels.');
386
    }
387
    if (!validateSampleRate(
388
        this.fmt.numChannels, this.fmt.bitsPerSample, this.fmt.sampleRate)) {
389
      throw new Error('Invalid sample rate.');
390
    }
391
  }
392
}
393
394
/**
395
 * Interleave de-interleaved samples.
396
 * @param {!Array<number>|!Array<!Array<number>>|!TypedArray} samples
397
 *    The samples.
398
 * @return {!Array<number>|!Array<!Array<number>>|!TypedArray}
399
 * @private
400
 */
401
function interleave(samples) {
402
  if (samples.length > 0) {
403
    if (samples[0].constructor === Array) {
404
      /** @type {!Array<number>} */
405
      let finalSamples = [];
406
      for (let i = 0, len = samples[0].length; i < len; i++) {
407
        for (let j = 0, subLen = samples.length; j < subLen; j++) {
408
          finalSamples.push(samples[j][i]);
409
        }
410
      }
411
      samples = finalSamples;
412
    }
413
  }
414
  return samples;
415
}
416
417
/**
418
 * Return the value for dwChannelMask according to the number of channels.
419
 * @param {number} numChannels the number of channels.
420
 * @return {number} the dwChannelMask value.
421
 * @private
422
 */
423
function dwChannelMask(numChannels) {
424
  /** @type {number} */
425
  let mask = 0;
426
  // mono = FC
427
  if (numChannels === 1) {
428
    mask = 0x4;
429
  // stereo = FL, FR
430
  } else if (numChannels === 2) {
431
    mask = 0x3;
432
  // quad = FL, FR, BL, BR
433
  } else if (numChannels === 4) {
434
    mask = 0x33;
435
  // 5.1 = FL, FR, FC, LF, BL, BR
436
  } else if (numChannels === 6) {
437
    mask = 0x3F;
438
  // 7.1 = FL, FR, FC, LF, BL, BR, SL, SR
439
  } else if (numChannels === 8) {
440
    mask = 0x63F;
441
  }
442
  return mask;
443
}
444
445
/**
446
 * Validate the number of channels in a wav file according to the
447
 * bit depth of the audio.
448
 * @param {number} channels The number of channels in the file.
449
 * @param {number} bits The number of bits per sample.
450
 * @return {boolean} True is the number of channels is valid.
451
 * @private
452
 */
453
function validateNumChannels(channels, bits) {
454
  /** @type {number} */
455
  let blockAlign = channels * bits / 8;
456
  if (channels < 1 || blockAlign > 65535) {
457
    return false;
458
  }
459
  return true;
460
}
461
462
/**
463
 * Validate the sample rate value of a wav file according to the number of
464
 * channels and the bit depth of the audio.
465
 * @param {number} channels The number of channels in the file.
466
 * @param {number} bits The number of bits per sample.
467
 * @param {number} sampleRate The sample rate to be validated.
468
 * @return {boolean} True is the sample rate is valid, false otherwise.
469
 * @private
470
 */
471
function validateSampleRate(channels, bits, sampleRate) {
472
  /** @type {number} */
473
  let byteRate = channels * (bits / 8) * sampleRate;
474
  if (sampleRate < 1 || byteRate > 4294967295) {
475
    return false;
476
  }
477
  return true;
478
}
479